package ndquangr.intern.ringtonemaker;

//by Evan X. Merz
//www.thisisnotalabel.com

//Example Wav file input and output
//this was written for educational purposes, but feel free to use it for anything you like 
//as long as you credit me appropriately ("wav IO based on code by Evan Merz")

//if you catch any bugs in this, or improve upon it significantly, send me the changes
//at evan at thisisnotalabel dot com, so we can share your changes with the world

import java.io.*;
import java.nio.ShortBuffer;

public class WavIO
{
/*
  WAV File Specification
  FROM http://ccrma.stanford.edu/courses/422/projects/WaveFormat/
 The canonical WAVE format starts with the RIFF header:
 0         4   ChunkID          Contains the letters "RIFF" in ASCII form
                                (0x52494646 big-endian form).
 4         4   ChunkSize        36 + SubChunk2Size, or more precisely:
                                4 + (8 + SubChunk1Size) + (8 + SubChunk2Size)
                                This is the size of the rest of the chunk 
                                following this number.  This is the size of the 
                                entire file in bytes minus 8 bytes for the
                                two fields not included in this count:
                                ChunkID and ChunkSize.
 8         4   Format           Contains the letters "WAVE"
                                (0x57415645 big-endian form).

 The "WAVE" format consists of two subchunks: "fmt " and "data":
 The "fmt " subchunk describes the sound data's format:
 12        4   Subchunk1ID      Contains the letters "fmt "
                                (0x666d7420 big-endian form).
 16        4   Subchunk1Size    16 for PCM.  This is the size of the
                                rest of the Subchunk which follows this number.
 20        2   AudioFormat      PCM = 1 (i.e. Linear quantization)
                                Values other than 1 indicate some 
                                form of compression.
 22        2   NumChannels      Mono = 1, Stereo = 2, etc.
 24        4   SampleRate       8000, 44100, etc.
 28        4   ByteRate         == SampleRate * NumChannels * BitsPerSample/8
 32        2   BlockAlign       == NumChannels * BitsPerSample/8
                                The number of bytes for one sample including
                                all channels. I wonder what happens when
                                this number isn't an integer?
 34        2   BitsPerSample    8 bits = 8, 16 bits = 16, etc.

 The "data" subchunk contains the size of the data and the actual sound:
 36        4   Subchunk2ID      Contains the letters "data"
                                (0x64617461 big-endian form).
 40        4   Subchunk2Size    == NumSamples * NumChannels * BitsPerSample/8
                                This is the number of bytes in the data.
                                You can also think of this as the size
                                of the read of the subchunk following this 
                                number.
 44        *   Data             The actual sound data.


NOTE TO READERS:

The thing that makes reading wav files tricky is that java has no unsigned types.  This means that the
binary data can't just be read and cast appropriately.  Also, we have to use larger types
than are normally necessary.

In many languages including java, an integer is represented by 4 bytes.  The issue here is
that in most languages, integers can be signed or unsigned, and in wav files the  integers
are unsigned.  So, to make sure that we can store the proper values, we have to use longs
to hold integers, and integers to hold shorts.

Then, we have to convert back when we want to save our wav data.

It's complicated, but ultimately, it just results in a few extra functions at the bottom of
this file.  Once you understand the issue, there is no reason to pay any more attention
to it.


ALSO:

This code won't read ALL wav files.  This does not use to full specification.  It just uses
a trimmed down version that most wav files adhere to.


*/

	// our private variables
	private String myPath;
	private long myChunkSize;
	private long mySubChunk1Size;
	private int myFormat;
	private long myChannels;
	private long mySampleRate;
	private long myByteRate;
	private int myBlockAlign;
	private int myBitsPerSample;
	private long myDataSize;
	private int myDuration;
		
	// I made this public so that you can toss whatever you want in here
	// maybe a recorded buffer, maybe just whatever you want
	//public byte[] myData;

	// get/set for the Path property
	public String getPath()
	{
		return myPath;
	}
	public void setPath(String newPath)
	{
		myPath = newPath;
	}

	// empty constructor
	public WavIO()
     {
		myPath = "";
     }

	// constructor takes a wav path
	public WavIO(String tmpPath)
     {
		myPath = tmpPath;
     }

	// read a wav file into this class
	public boolean read(short [] data)
	{
		DataInputStream inFile = null;
		//myData = null;
		byte[] tmpLong = new byte[4];
		byte[] tmpInt = new byte[2];

		try
		{
			inFile = new DataInputStream(new FileInputStream(myPath));

			//System.out.println("Reading wav file...\n"); // for debugging only

			String chunkID = "" + (char)inFile.readByte() + (char)inFile.readByte() + (char)inFile.readByte() + (char)inFile.readByte();

			inFile.read(tmpLong); // read the ChunkSize
			myChunkSize = byteArrayToLong(tmpLong);

			String format = "" + (char)inFile.readByte() + (char)inFile.readByte() + (char)inFile.readByte() + (char)inFile.readByte();

			String subChunk1ID = "" + (char)inFile.readByte() + (char)inFile.readByte() + (char)inFile.readByte() + (char)inFile.readByte();

			inFile.read(tmpLong); // read the SubChunk1Size
			mySubChunk1Size = byteArrayToLong(tmpLong);

			inFile.read(tmpInt); // read the audio format.  This should be 1 for PCM
			myFormat = byteArrayToInt(tmpInt);

			inFile.read(tmpInt); // read the # of channels (1 or 2)
			myChannels = byteArrayToInt(tmpInt);
			
			inFile.read(tmpLong); // read the samplerate
			mySampleRate = byteArrayToLong(tmpLong);

			inFile.read(tmpLong); // read the byterate
			myByteRate = byteArrayToLong(tmpLong);

			inFile.read(tmpInt); // read the blockalign
			myBlockAlign = byteArrayToInt(tmpInt);

			inFile.read(tmpInt); // read the bitspersample
			myBitsPerSample = byteArrayToInt(tmpInt);

			// read the data chunk header - reading this IS necessary, because not all wav files will have the data chunk here - for now, we're just assuming that the data chunk is here
			String dataChunkID = "" + (char)inFile.readByte() + (char)inFile.readByte() + (char)inFile.readByte() + (char)inFile.readByte();

			inFile.read(tmpLong); // read the size of the data
			myDataSize = byteArrayToLong(tmpLong);
			
			int numberByteRead = (int)((myBitsPerSample/8)*myChannels); // Read 1 sample for one channel
			byte [] tempRead = new byte [numberByteRead]; // Store value of one sample for every channels
			short [] tempShort = new short [(int) myChannels];
			int skippedBytes = (int)(mySampleRate*numberByteRead/100) - numberByteRead; // Skip this byte to reduce reading time
			int countSampleRead = 0; // Number of samples read to data set
			int i, sum;
			while (true) {
				if(inFile.read(tempRead)== -1) {
					break;
				}
				if (countSampleRead >= data.length) {
					break;
				}
				tempShort = byteArrayToShortArray(tempRead);
				sum = 0;
				for(i=0; i<myChannels; i++) {					
					sum += tempShort[i];
				}
				data[countSampleRead++] = (short) (sum/myChannels);
				inFile.skipBytes(skippedBytes);
			}			
			// close the input stream
			inFile.close();
		}
		catch(Exception e)
		{
			return false;
		}

		return true; // this should probably be something more descriptive
	}

	// write out the wav file
	public boolean save(byte [] data)
	{
		try
		{
			DataOutputStream outFile  = new DataOutputStream(new FileOutputStream(myPath));

			// write the wav file per the wav file format
			outFile.writeBytes("RIFF");					// 00 - RIFF
			outFile.write(intToByteArray((int)myChunkSize), 0, 4);		// 04 - how big is the rest of this file?
			outFile.writeBytes("WAVE");					// 08 - WAVE
			outFile.writeBytes("fmt ");					// 12 - fmt 
			outFile.write(intToByteArray((int)mySubChunk1Size), 0, 4);	// 16 - size of this chunk
			outFile.write(shortToByteArray((short)myFormat), 0, 2);		// 20 - what is the audio format? 1 for PCM = Pulse Code Modulation
			outFile.write(shortToByteArray((short)myChannels), 0, 2);	// 22 - mono or stereo? 1 or 2?  (or 5 or ???)
			outFile.write(intToByteArray((int)mySampleRate), 0, 4);		// 24 - samples per second (numbers per second)
			outFile.write(intToByteArray((int)myByteRate), 0, 4);		// 28 - bytes per second
			outFile.write(shortToByteArray((short)myBlockAlign), 0, 2);	// 32 - # of bytes in one sample, for all channels
			outFile.write(shortToByteArray((short)myBitsPerSample), 0, 2);	// 34 - how many bits in a sample(number)?  usually 16 or 24
			outFile.writeBytes("data");					// 36 - data
			outFile.write(intToByteArray((int)myDataSize), 0, 4);		// 40 - how big is this data chunk
			outFile.write(data);						// 44 - the actual data itself - just a long string of numbers
		}
		catch(Exception e)
		{
			System.out.println(e.getMessage());
			return false;
		}

		return true;
	}
	
	// Write a part of wav to new file
	public boolean save(int startTime, int endTime, String filePath) {
		//DataOutputStream outFile  = new DataOutputStream(new FileOutputStream(filePath));
		try {
			float oneMsSecBytes = (float) (0.001*(mySampleRate*myChannels*myBitsPerSample/8));
			DataInputStream inFile = new DataInputStream(new FileInputStream(myPath));
			long skippedBytes = (int) (oneMsSecBytes*(startTime));
			inFile.skip((int) skippedBytes + 44);
			
			// Re-calculate data size
			long datasize =(int) (oneMsSecBytes*(endTime - startTime));
			
			DataOutputStream outFile  = new DataOutputStream(new FileOutputStream(filePath));
			outFile.writeBytes("RIFF");					// 00 - RIFF
			outFile.write(intToByteArray((int)(datasize+36)), 0, 4);		// 04 - how big is the rest of this file?
			outFile.writeBytes("WAVE");					// 08 - WAVE
			outFile.writeBytes("fmt ");					// 12 - fmt 
			outFile.write(intToByteArray((int)mySubChunk1Size), 0, 4);	// 16 - size of this chunk
			outFile.write(shortToByteArray((short)myFormat), 0, 2);		// 20 - what is the audio format? 1 for PCM = Pulse Code Modulation
			outFile.write(shortToByteArray((short)myChannels), 0, 2);	// 22 - mono or stereo? 1 or 2?  (or 5 or ???)
			outFile.write(intToByteArray((int)mySampleRate), 0, 4);		// 24 - samples per second (numbers per second)
			outFile.write(intToByteArray((int)myByteRate), 0, 4);		// 28 - bytes per second
			outFile.write(shortToByteArray((short)myBlockAlign), 0, 2);	// 32 - # of bytes in one sample, for all channels
			outFile.write(shortToByteArray((short)myBitsPerSample), 0, 2);	// 34 - how many bits in a sample(number)?  usually 16 or 24
			outFile.writeBytes("data");					// 36 - data			
			outFile.write(intToByteArray((int)datasize), 0, 4);		// 40 - how big is this data chunk
			byte [] temp = new byte [(int) (oneMsSecBytes)];
			int count = 0;
			// Write data to file
			while(true) {
				if (count > (endTime - startTime)) {
					break;
				}
				if (inFile.read(temp) == -1) {
					break;
				}				
				outFile.write(temp);
				count++;
			}
			
			inFile.close();
			outFile.flush();
			outFile.close();
			return true;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return false;
		}
	}

	// return a printable summary of the wav file
	public String getSummary()
	{
		//String newline = System.getProperty("line.separator");
		String newline = "<br>";
		String summary = "<html>Format: " + myFormat + newline + "Channels: " + myChannels + newline + "SampleRate: " + mySampleRate + newline + "ByteRate: " + myByteRate + newline + "BlockAlign: " + myBlockAlign + newline + "BitsPerSample: " + myBitsPerSample + newline + "DataSize: " + myDataSize + "</html>";
		return summary;
	}
	
	public long getDataSize() {	
		return myDataSize;
	}


//===========================
//CONVERT BYTES TO JAVA TYPES
//===========================

	// these two routines convert a byte array to a unsigned short
	public static int byteArrayToInt(byte[] b)
	{
		int start = 0;
		int low = b[start] & 0xff;
		int high = b[start+1] & 0xff;
		return (int)( high << 8 | low );
	}


	// these two routines convert a byte array to an unsigned integer
	public static long byteArrayToLong(byte[] b)
	{
		int start = 0;
		int i = 0;
		int len = 4;
		int cnt = 0;
		byte[] tmp = new byte[len];
		for (i = start; i < (start + len); i++)
		{
			tmp[cnt] = b[i];
			cnt++;
		}
		long accum = 0;
		i = 0;
		for ( int shiftBy = 0; shiftBy < 32; shiftBy += 8 )
		{
			accum |= ( (long)( tmp[i] & 0xff ) ) << shiftBy;
			i++;
		}
		return accum;
	}


//===========================
//CONVERT JAVA TYPES TO BYTES
//===========================
	// returns a byte array of length 4
	public static byte[] intToByteArray(int i)
	{
		byte[] b = new byte[4];
		b[0] = (byte) (i & 0x00FF);
		b[1] = (byte) ((i >> 8) & 0x000000FF);
		b[2] = (byte) ((i >> 16) & 0x000000FF);
		b[3] = (byte) ((i >> 24) & 0x000000FF);
		return b;
	}

	// convert a short to a byte array
	public static byte[] shortToByteArray(short data)
	{
		return new byte[]{(byte)(data & 0xff),(byte)((data >>> 8) & 0xff)};
	}
	
	/**
	public static float Convert4BytesToFloat(byte byte1, byte byte2, byte byte3, byte byte4)
	{
		int l,ll,m,e;
		float returnvalue=0,mantisse=0;
	 
		l = byte1;
		l = l << 24;
		ll = byte2;
		l = l | (ll << 16);
		ll = byte3;
		l = l | (ll << 8);
		ll = byte4;
		l = l | ll;
	 
		e=l&0x7f800000;
		e>>=23;
		m=l&0x7fffff;
	 
		int index;
		for(int i=0; i<=23;i++)
		{		
			if(CHECK_BIT(m,i))
			{
				//#define CHECK_BIT(var,pos) ((var)&(1<<(pos)))
				//ldexp(1, i-23);
				mantisse += float(ldexp((float)1, i-23));//float(ldexp(1, i-23));//float((ldexp(1, index-23)));
			}
		}
	 
		returnvalue=float(ldexp(1+mantisse, e-127));
		if (CHECK_BIT(l,31)) {
			returnvalue = (-1)*returnvalue;
		} 
	 
		return returnvalue;
	 }
	*/
	public static short [] byteArrayToShortArray(byte [] byteArray) {		
		int length = byteArray.length;
		short [] intArray = new short [length/2];
		for (int i=0;i<length;i=i+2) {
			intArray[i/2] = (short) (((byteArray[i + 1] & 0xff) << 8) | (byteArray[i] & 0xff));

		}
		return intArray;
		
	}
	
	public static byte[] shortToByteArray(ShortBuffer shortBuffer, int size) {
		//short[] shortArray = new short [size];
		//shortBuffer.position(0);
		//shortBuffer.get(shortArray );
		byte[] bytearray = new byte [size*2];
		byte [] tempbyte;
		for (int i = 0; i< size; i++) {
			tempbyte = shortToByteArray(shortBuffer.get(i));
			bytearray[i*2] = tempbyte[0];
			bytearray[i*2+1] = tempbyte[1];
		}
		return bytearray;
	}
}